/*
* GeoVISTA Center (Penn State, Dept. of Geography)
* Copyright (c), 2003-2011,  Jin Chen and Alan M. MacEachren, GeoVISTA Center, Penn State University
* All Rights Researved.
*
* A changable-size model. It means you can add/remove both records and attributes to the model
*
*
*
*  
*
* Assume data belonged to a column are of same type.
* @Original Author: jin Chen
* @date: Jul 21, 2003$
* @version: 1.0
*/

package edu.psu.geovista.data.model;

import edu.psu.geovista.data.geog.DataSetForApps;

import java.awt.*;
import java.awt.geom.Point2D;
import java.util.*;
import java.util.List;



public class DefaultPlotModel extends DefaultAppDataModel implements edu.psu.geovista.data.model.AppDataModel,Cloneable, PlotModel {
    public static final boolean DEBUG=false;
    //public static final int NULL_INT = Integer.MIN_VALUE ;//.MAX_VALUE;
    /** Contains the float[] values. */
    // protected Vector values = new Vector();  //assume contain numeric values only



    /** Contains the axis label(name) Strings. */
    // protected ColumnList columns=new ColumnList(); //This is the column names for (numeric)values only, not include name for shape or observation name column
    /** Contains the value label Hashtables. */
    protected Vector valueLabels; //columnName?

    //protected Shape[] shpData;

    /** Number of dimensions of the model. */
    //protected int numDimensions = 0;

    //A column may have some specific value which may determine its GUI.
    //E.g.: For a timeseries plot, a column value may be the year represent by the column.
    //      The value determine the location of the axis representing the column.

    protected Hashtable columnValues=new Hashtable();

    /** List to store our event subscribers. */
    //protected EventListenerList listenerList = new EventListenerList();





    /*
     *For regenerate rawData.
     */
    //protected Object[] raw;
    private List originalNumericColNames;

    /*******************************************************************************************************
     *                Construction
     *******************************************************************************************************/

    public  DefaultPlotModel() {

    }
    private DefaultPlotModel(DataSetForApps dataSet){
        /*Object[] rawData=dataSet.getDataObjectOriginal();
        this.feedData(rawData);*/
        super(dataSet);

    }
    /**
     *  Copyied from constructor of STFDataSet.java to make the model compatible with GeoVista
     * @param rawData
     */
    private DefaultPlotModel(Object[] rawData) {//
        //this.feedData(rawData);
        super(rawData);

    }

    protected  void initColumns(int num) {
        //this.numDimensions =num;
        this.values=new Vector(num);
        this.columns =new ColumnList(num);
        this.valueLabels =new Vector(num);

        for (int i=0; i<num; i++){
            columns.add(null);
            valueLabels.add(null);
        }



    }

    /**
     *
     * @param rawData STF data: A column based data. First element is an array of column header
     *                          Other elements are array of data
     */
    public void feedData(Object[] rawData) {
        this.setRaw(new Object[rawData.length ]);
        System.arraycopy(rawData,0,raw,0,raw.length );

        Object[] dataIn = rawData;
        Object[] dataToShow = null; //Each element is an array
        int[] orignalToNew = new int[dataIn.length];
        String[] varNames = (String[])dataIn[0];
        boolean[] isNumeric = new boolean[dataIn.length];
        int count = 0;
        for (int i = 0; i < dataIn.length; i++) {
            Object obj = dataIn[i];
            //Determine if a data is numeric or not. Only double[] and int[] is treated as numeric
            if (obj instanceof double[] || obj instanceof int[]) {
                isNumeric[i] = true;
                count++;
            } else {
                isNumeric[i] = false;
            }
        }
        if (count == 0) return;

        String[] numericVarNames = new String[count];
        dataToShow = new Object[count + 3]; //one for variable names,one for observation names,one for spatial data;

        count = 0;
        for (int i = 1; i < isNumeric.length; i++) { //isNumeric.length= dataIn.length
            if (isNumeric[i]){
                dataToShow[count+1] = dataIn[i];
                numericVarNames[count] = varNames[i-1]; // -1 because of the varNames themselves
                orignalToNew[i] = count;//so we can get back if need be
                count++;
            }
            else{  //non numeric data excluding any shape data since shape data will be treated specially later
                Object[] nonNumeric=(Object[]) dataIn[i];
                if(! (nonNumeric instanceof Shape[])&&
                        i<=varNames.length ){
                    //varNames not including name of shape column thus, if want to add shape data here => ArrayOutofIndexException
                    //varNames = (String[])dataIn[0];(column names). However, some column don't have name.e.g.: Shape column but it is array of GeneralPath[].
                    //Any way, only keep data has name
                    String name=varNames[i-1]; // -1 because of the varNames themselves
                    this.addNonNumericData(name, nonNumeric);
                }


            }
        }
        dataToShow[0] = numericVarNames;
        for (int i = 0; i < varNames.length; i++) {
            String lower = varNames[i].toLowerCase();
            /** Observation names:
             *   By default, any String column whose name contain "name" will be treated as observation name column
             *   Now dataToShow[numericVarNames.length +1]  contain ob names. Ob name is set together with adding record
             *   see the bottom of the method
             *
             */
            if (lower.endsWith("name") && (dataIn[i + 1] instanceof String[])){
                dataToShow[numericVarNames.length +1] = (String[])dataIn[i+1];
                //this.spat.setObservationNames((String[])dataIn[i+1]);//+1 to skip varNames
            }
        }

        this.initColumns(numericVarNames.length);
        this.setColumnNames(numericVarNames);
        Object obj = dataToShow[1];
        double[] someDoubles = null;
        int[] someInts = null;
        int numObservations = -1;
        if (obj instanceof double[]) {
            someDoubles = (double[])obj;
            numObservations = someDoubles.length;
        } else if (obj instanceof int[]) {
            someInts = (int[])obj;
            numObservations = someInts.length;
        }


        String name = null;
        for (int row = 0; row < numObservations; row++) {
            float[] dataVals = new float[numericVarNames.length];
            for (int column = 1; column < dataVals.length + 1; column++) {
                obj = dataToShow[column];
                if (obj instanceof double[]) {
                    someDoubles = (double[])obj;
                    if(Double.isNaN(someDoubles[row]) )
                        dataVals[column-1]=Float.NaN ; // 
                    else
                        dataVals[column-1] = (float)someDoubles[row];

                } else if (obj instanceof int[]) {
                    someInts = (int[])obj;
                    /*if(someInts[row]<AppDataModel.NULL_INT +10) {
                    System.out.println("get:"+someInts[row]);
                    System.out.println("AppDataModel.NULL_INT :"+AppDataModel.NULL_INT );
                    }*/
                    if(someInts[row]<= AppDataModel.NULL_INT+1 ) {
                        /* Jin: Hand null value
                        A bug in ShapeFileReader as well as in CSVFileReader: sometime it treat null as
                        -Integer.MAX_VALUE  and sometime Integer.MIN_VALUE.
                        To read data from these readers, treat int value equal to either extreme value as null
                        */
                        dataVals[column-1]=Float.NaN ;
                    }
                    else
                        dataVals[column-1] = (float)someInts[row];
                }//end if
            }//next column

            /**
             *  Observatoin name(record name).
             * If there is meta file to specify ob names, it will replace this one.
             */
            if (dataToShow[numericVarNames.length + 1] != null) {
                String[] names = (String[])dataToShow[numericVarNames.length +1];//
                name = names[row]; //name is observation name
            }
            this.addRow(dataVals, name);
        }//next row

        //shape data
        for (int i = 0; i < rawData.length; i++) {
            if (rawData[i] instanceof Shape[]){
                Shape[] shp=(Shape[]) rawData[i];
                this.setShpData(shp);
                break;
            }
            else if(rawData[i] instanceof Point2D[]) {
                assert false:"Only accept Shape type, not Point2D type!";

                break;

            }
        }


        this.setOriginalNumericColNames(this.columns.getColumnNameList()) ;
        /* out Sept 4, not compatible with Jame's FeatureToSet
        Shape[] shp=(Shape[]) rawData[rawData.length -2];
        Object attribute=rawData[rawData.length -1];
        this.setShpData(shp);
        System.out.println("finish");*/
    }

    /*******************************************************************************************************
     *                Row(Record) operations
     *******************************************************************************************************/




    /**
     * Returns all values of a specific record.
     *
     * @param record The number of the record to be returned.
     *
     * @return All values of the specified record..
     */
    /*public float[] getRowValueAsFloat(int recordnum) {
        //return (float[])values.elementAt(recordnum);
        return convertVectorToFloat((Vector)values.elementAt(recordnum));
    }*/







    /*******************************************************************************************************
     *                Index operations
     *******************************************************************************************************/






    /**
     * Set time series value
     * @param colID  column ID
     * @param value
     */
    public void addColumnValues(String colID, float value) {
        this.getMetaData().setColumnValue(colID,value);
        // this.columnValues.put(colID,new Float(value));//
    }



    public Map getColumnValues() {
        if(columnValues!=null&&columnValues.size() >0)
                return columnValues;//
        else
            return  getMetaData().getColumnValues();
    }
    public MetaData getMetaData(){
        /* if(meta==null){
            meta= new EstatMetaData();//temp
        }*/
        return meta;
    }
    /**
     *
     * @return  min and max of column value
     */
   /*public Range getColumnValuesExtreme() {
        
       /*return meta.getColumnValuesExtreme();
   }  */
    /*******************************************************************************************************
     *                Value labels
     *******************************************************************************************************/
    /**
     * Returns a Hashtable with labels for specific values. This is provided for
     * ordinal values, which might be added as keys to the Hashtable, with the
     * corresponding human-readable labels as values.
     *
     * @param dimension The dimension to retrieve value labels for.
     *
     * @return A Hashtable containing value-label pairs.
     */
    public Hashtable getValueLabels(int dimension) {
        //return valueLabels[dimension];
        return (Hashtable) valueLabels.get(dimension);
    }

    /**
     * Returns the label for a single value in a specific dimension, if present.
     *
     * @param dimension The dimension.
     * @param value The value to look up a label for.
     *
     * @return A String with the label, null if no label is set.
     */
    public String getValueLabel(int dimension, float value){
        Hashtable vl=(Hashtable) valueLabels.get(dimension);
        if (vl!=null){
            return (String) (vl.get(new Float(value)));
        }
        else{
            return null;
        }

        /* if (valueLabels[dimension] != null){
        return (String) (valueLabels[dimension].get(new Float(value)));
        }
        else {
        return null;
        }*/
    }

    /**
     * Sets the value labels for a dimension.
     * Note that this method is not included in the ParallelSpaceModel interface,
     * which defines only read-only methods. It is used for filling the model
     * before passing it on to a consumer.
     *
     * @param dimension The dimension the labels are to be set for.
     * @param values The values to assign labels to. Note that the number of labels an values must match.
     * @param labels The String labels for the values. Note that the number of labels an values must match.
     */
    public void setValueLabels(int dimension, float values[], String labels[]){
        /*if (values.length != labels.length) {
        throw new IllegalArgumentException("number of values and labels do not match!");
        }

        if (valueLabels[dimension] == null){
        valueLabels[dimension] = new Hashtable();
        }

        for (int i=0; i<values.length; i++){
        valueLabels[dimension].put(new Float(values[i]), labels[i]);
        }*/
        if (values.length != labels.length) {
            throw new IllegalArgumentException("number of values and labels do not match!");
        }

        for (int i=0; i<values.length; i++){
            this.setValueLabel(dimension,values[i],labels[i]);
        }

    }

    /**
     * Sets a single value label for a specific axis.
     *
     * @param dimension The dimension to set the label for.
     * @param value The value to set the label for.
     * @param label The label to set.
     */
    public void setValueLabel(int dimension, float value, String label){

        /*if (valueLabels[dimension] == null){
        valueLabels[dimension] = new Hashtable();
        }

        valueLabels[dimension].put(new Float(value), label);*/
        Hashtable valueLabel=(Hashtable) this.valueLabels.get(dimension);
        if (valueLabel == null){
            valueLabel = new Hashtable();
        }

        valueLabel.put(new Float(value), label);

    }

    /*******************************************************************************************************
     *                support
     *******************************************************************************************************/





    /*******************************************************************************************************
     *                Properties
     *******************************************************************************************************/
    /**
     *
     * Return data in the original format. We can't return just original data(so-called:rawData) since
     * rawData's format is used for data communication between Map and Scatterplot and other old Beans.
     * rawData must contain shape data, otherwise map won't work.  However, since with NCI project, shp data
     * and observation data are loaded seperated, which means rawData do NOT have shape data. We need add shape
     * data to rawData before send it to Map or other beans. Thus don't just return rawData.
     *
     *  
     *  
     *  
     *  
     *
     *  
     *
     *  
     *  
     *  
     *  
     *  
     *  
     *
     *  
     *  
     *  
     *  
     * @return       Object[]
     */
   public Object[] getGeoVistaRawData(){
        List curColNameList=this.columns.getColumnNameList() ;
        //curColNameList.addAll("FIPS"); if need to keep FIPS code, add this line, but make sure the column name of FIPS is "FIPS"
        //
        List needRemoved=new ArrayList( );
        //initialize to the size
        List tmp=Collections.nCopies(getOriginalNumericColNames().size(),null);
        needRemoved.addAll(tmp);
        Collections.copy(needRemoved,getOriginalNumericColNames());

        boolean removed=needRemoved.removeAll(curColNameList); //?needRemoved: non-numerical
        if(!removed){
            //todo exception
        }

        ArrayList datalist=new ArrayList();
        if(raw==null)return new Object[0];
        //.  Header
        String[] header=(String[]) raw[0]; //header
        int headerlen = header.length;
        ArrayList headerList=new ArrayList();



        //list.add(header);//add header
        boolean hasShapeData=false;
        for (int i=0;i<headerlen ;i++){ //int i=0;i<header.length ;i++
            if(needRemoved.contains(header[i])){//remove data name
                headerList.remove(header[i]);//if remove data, also need remove data column name from header of resulted rawData
            }
            else{
                //keep ONLY the column not in remove list
                Object col= raw[i+1];//1st element is header
                if(col instanceof String[]){ //change all String data to String[] type
                    String[] str=(String[]) col;
                    datalist.add(str);
                    headerList.add(header[i]);
                }
                else{
                    datalist.add(col);
                    headerList.add(header[i]);
                }


                if(col instanceof Shape[] && ((Shape[]) col).length >0){
                    hasShapeData=true;
                }
            }
        }

       makeNameHeader(this,datalist, headerList);

        //change all String data to String[] type
        String[] finalHeader=new String[headerList.size()];
        for (int i=0;i<headerList.size() ;i++){
            finalHeader[i]=(String) headerList.get(i);
        }
        //String[] finalHeader=(String[]) headerList.toArray();


        datalist.add(0, finalHeader);
        if(!hasShapeData){
            Shape[] shp=this.getShpData() ;
            if(shp!=null&&shp.length >0)
                datalist.add(datalist.size(),shp);  //add to end
        }
        Object[] myRaw= datalist.toArray();
        //Object raw=this.raw ;
        return myRaw ;
    }


    /* public Object[] getGeoVistaRawData0(){
            Object[] rd=new Object[this.getNumDimensions()+4];//one for header, one for rowName,one for shape data, one for attribute
            Vector hv=(Vector) this.columns.clone() ;
            hv.insertElementAt(new Index("RowName"),0);

            Object[] hd=hv.toArray() ;
            String[] header=new String[hd.length ];
            for (int i=0;i<hd.length ;i++){
                header[i]=((Index)hd[i]).getName() ;
            }


            String[] rowName=convertListToString(this.rowNames); ;
            //construct rawDat
            rd[0]=header;
            rd[1]=rowName;
            for (int i=0;i<this.getNumDimensions() ;i++){
                rd[i+2]=this.getColumnValueAsDouble(i);

            }
            //shp
            Shape[] shp=this.getShpData() ;
            if(shp!=null){
                rd[rd.length -2]=shp;
                Object[] attr=new Object[shp.length ];//dumpy attribute data, temp solution
                rd[rd.length -1]=attr;
            }

            return rd;
        }
    */

    /*******************************************************************************************************
     *                clone
     *******************************************************************************************************/
    public Object clone() {
        return new CloneModel();
    }

    public static PlotModel createDefaultPlotModel() {
        return new DefaultPlotModel();
    }

    public static PlotModel createDefaultPlotModel(DataSetForApps dataSet) {
        return new DefaultPlotModel(dataSet);
    }

    public static PlotModel createDefaultPlotModel(Object[] rawData) {
        return new DefaultPlotModel(rawData);
    }

    public List getOriginalNumericColNames() {
        if(originalNumericColNames==null){
            originalNumericColNames=this.columns.getColumnNameList();
        }
        return originalNumericColNames;
    }

    public void setOriginalNumericColNames(List originalNumericColNames) {
        this.originalNumericColNames = originalNumericColNames;
    }

    public void setColumnValues(Hashtable columnValues) {
        this.columnValues = columnValues;
    }

    private class CloneModel extends DefaultPlotModel{
        public CloneModel() {
            deepCopy();
        }

        private void deepCopy() {
            // 
            this.meta=(MetaData) DefaultPlotModel.this.meta.clone();


            // 
            this.values=(Vector) DefaultPlotModel.this.values.clone() ;
            if(DefaultPlotModel.this.rowNames!=null)
                this.rowNames=(Vector) DefaultPlotModel.this.rowNames.clone() ;

            if(DefaultPlotModel.this.rowNameColumns !=null)
                this.rowNameColumns =DefaultPlotModel.this.rowNameColumns.clone() ;

            this.rowNameColumns =  DefaultPlotModel.this.rowNameColumns; //String, no need clone
            this.columns =(ColumnList) DefaultPlotModel.this.columns.clone() ;
            this.valueLabels = (Vector) DefaultPlotModel.this.valueLabels.clone();
            //this.numDimensions=numDimensions;
            //Time Series value
            //this.columnValues= (Hashtable) DefaultPlotModel.this.columnValues.clone() ; //since Hashtable only contain primitive's wrapper(key is String, Value is Integer), shallow copy is enough


            this.nvalues = (Vector) DefaultPlotModel.this.nvalues.clone() ;
            this.ncolumns =    (ColumnList) DefaultPlotModel.this.ncolumns.clone() ;

            this.setOriginalNumericColNames(new ArrayList());
            for (int i=0;i<DefaultPlotModel.this.getOriginalNumericColNames().size() ;i++){
                getOriginalNumericColNames().add(null);
            }
            Collections.copy(this.getOriginalNumericColNames(),DefaultPlotModel.this.getOriginalNumericColNames()); //originalNumericColNames.clone
            //shape
            if(DefaultPlotModel.this.shpData!=null){
                this.shpData =new Shape[DefaultPlotModel.this.shpData.length ];
                System.arraycopy(DefaultPlotModel.this.shpData,0,this.shpData,0,shpData.length);
            }

            if(DefaultPlotModel.this.raw !=null){
                this.raw=new Object[DefaultPlotModel.this.raw.length ];
                System.arraycopy(DefaultPlotModel.this.raw,0,raw,0,raw.length );
            }
            if(Id2Index!=null)
                this.Id2Index =(Map) ((HashMap) DefaultPlotModel.this.Id2Index).clone();

            /* if(DefaultPlotModel.this.meta!=null){
                try {
                    //Object o = BeanUtils.cloneBean(DefaultPlotModel.this.meta);
                    //this.meta =(EstatMetaData) o;
                } catch (Exception e) {
                    e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
                }
            }*/

        }
    }




    /**
     * test performance for add&remove column
     * @param args
     */
    public static void main(String[] args) {
        PlotModel model=createDefaultPlotModel();
        //float[][] data=new float[3100][31];
        int rowCount=3110;

        for (int i=0;i<rowCount;i++){
            float[] row=new float[31];
            for (int j=0;j<row.length ;j++){
                row[j]=j;
            }
            model.addRow(row);
        }

        long start,stop; //put is at start of method
        if(DEBUG){//put is at start of method
            start=Calendar.getInstance().getTime().getTime() ;
        }
        int numCol=model.getColumnCount() ;
        int numRow=model.getRowCount() ;
        System.out.println("before remove column10:"+model.getValue(rowCount-1,10) );
        System.out.println("before remove column10:"+model.getValue(0,10) );
        model.removeColumn(10);
        System.out.println("after remove column10:"+model.getValue(rowCount-1,10) );
        System.out.println("after remove column10:"+model.getValue(0,10) );
        if(DEBUG){  //put is at end of method
            stop=Calendar.getInstance().getTime().getTime() ;
            System.out.println("removeColumn() take "+(stop-start)*0.001f+" to finish" );
        }
        // 
        float[] aRow=new float[rowCount];
        Arrays.fill(aRow,10.0f);

        if(DEBUG){//put is at start of method
            start=Calendar.getInstance().getTime().getTime() ;
        }
        System.out.println("before add column10:"+model.getValue(rowCount-1,10) );
        System.out.println("before add column10:"+model.getValue(0,10) );
        model.addColumn("10",aRow);
        System.out.println("after add column10:"+model.getValue(rowCount-1,10) );
        System.out.println("after add column10:"+model.getValue(0,10) );
        if(DEBUG){  //put is at end of method
            stop=Calendar.getInstance().getTime().getTime() ;
            System.out.println("addColumn() take "+(stop-start)*0.001f+" to finish" );
        }
        // 




    }



}

/**
 *
 * @param values A float[][] is an array of records (first index).
 *               Each record is an array with float values for each dimension (second index).
 *               All records must have the same number of dimensions!
 */
/*public DefaultPlotModel(float values[][]) {
int i;
int len = values[0].length;

initNumDimensions(len);

for (i=0; i<values.length; i++){
if (values[i].length != len) {
throw new IllegalArgumentException("Recordsets must have same number of dimensions");
}
}

for (i=0; i<values.length; i++){
this.values.addElement(values[i]);
}

}*/
